Skip to content

type:story Add Parquet DESCRIPTOR mode for blob inline reading#18683

Open
rahil-c wants to merge 2 commits into
apache:masterfrom
rahil-c:feat/parquet-blob-descriptor-mode
Open

type:story Add Parquet DESCRIPTOR mode for blob inline reading#18683
rahil-c wants to merge 2 commits into
apache:masterfrom
rahil-c:feat/parquet-blob-descriptor-mode

Conversation

@rahil-c

@rahil-c rahil-c commented May 3, 2026

Copy link
Copy Markdown
Collaborator

Describe the issue this Pull Request addresses

When hoodie.read.blob.inline.mode=DESCRIPTOR is set with Parquet base files, leverage Parquet's nested column projection to skip reading the blob data sub-column entirely (genuine I/O savings). Previously the config only affected Lance reads; Parquet still materialized the bytes.

Approach mirrors the existing VECTOR column rewrite pattern in HoodieFileGroupReaderBasedFileFormat:

  1. Detect blob columns via schema metadata.
  2. Strip the data sub-field from blob structs in the read schema.
  3. Post-read null-pad the data field back into output rows.

Both COW (HoodieFileGroupReaderBasedFileFormat.readBaseFile) and MOR (SparkFileFormatInternalRowReaderContext.getFileRecordIterator) paths are covered. Also adds a defensive null check in BatchedBlobReader.

A naive DESCRIPTOR + read_blob() would silently return null on Parquet (no byte-range channel like Lance). To keep the API consistent, ReadBlobRule now downgrades any Parquet scan to CONTENT for queries that contain read_blob(), while sibling queries on the same FileFormat instance keep DESCRIPTOR's I/O savings.

Summary and Changelog

User-visible behavior

  • hoodie.read.blob.inline.mode=DESCRIPTOR now works on Parquet base files (COW + MOR), not just Lance, and skips the blob data Parquet column for real I/O savings.
  • read_blob() keeps working under DESCRIPTOR on Parquet — the engine automatically downgrades the affected scan to CONTENT so bytes are materialized; sibling queries that don't use read_blob() still benefit from DESCRIPTOR.

Detailed changelog

DESCRIPTOR-on-Parquet:

  • VectorConversionUtils: new helpers detectBlobColumnsFromMetadata, stripBlobDataField, buildBlobNullPadRowMapper.
  • HoodieFileGroupReaderBasedFileFormat:
    • supportBatch returns false when DESCRIPTOR is active and blob columns are present (row-level access required for null-padding).
    • readBaseFile strips the data sub-field from the read schema and wraps the iterator with wrapWithBlobNullPadding.
  • SparkFileFormatInternalRowReaderContext.getFileRecordIterator: same rewrite/pad on the MOR base-file path, driven by the Hadoop conf entry.
  • HoodieReaderConfig.BLOB_INLINE_READ_MODE: docstring updated to describe Parquet semantics.
  • BatchedBlobReader: defensive null check on the data row.
  • HoodieHadoopFsRelationFactory: pass through the configured DESCRIPTOR flag.

read_blob() override:

  • HoodieFileGroupReaderBasedFileFormat: constructor flag renamed isBlobDescriptorModeinitialBlobDescriptorMode; new mutable _isBlobDescriptorMode with setBlobDescriptorMode / restoreBlobDescriptorMode. buildReaderWithPartitionValues syncs the Hadoop conf entry from the mutable flag so the MOR path agrees with the COW path after a flip.
  • ReadBlobRule: walks each plan it sees; if read_blob() (or an already-injected BatchedBlobRead) is present, flips DESCRIPTOR→CONTENT on every Hudi Parquet LogicalRelation's FileFormat. Otherwise restores the construction-time value (handles shared FileFormat instances across queries against the same temp view). Lance scans are skipped.
  • HoodieReaderConfig.BLOB_INLINE_READ_MODE: docstring updated to note the automatic downgrade.

Tests added

  • TestReadBlobSQL.testParquetDescriptorSkipsDataColumn@ParameterizedTest over HoodieTableType (COW + MOR): asserts INLINE type preserved, data null, reference null.
  • TestReadBlobSQL.testReadBlobSupersedesDescriptorOnParquetread_blob() materializes bytes despite DESCRIPTOR, then a follow-up query on the same view restores DESCRIPTOR's null-pad.
  • TestReadBlobSQL.testReadBlobInWhereClauseUnderDescriptor — override engages when read_blob() is in WHERE.
  • TestReadBlobSQL.testMultiBlobColumnsDescriptorWholeScanDowngraderead_blob() on one blob column also materializes bytes for unrelated blob columns in the same scan.
  • TestReadBlobSQL.testDescriptorOnTableWithoutBlobColumns — DESCRIPTOR on a non-blob table is a no-op.
  • New TestVectorConversionUtilsBlob — unit tests for detectBlobColumnsFromMetadata, stripBlobDataField, and buildBlobNullPadRowMapper.

Impact

  • User-facing: Parquet readers that opt into hoodie.read.blob.inline.mode=DESCRIPTOR now skip the blob data Parquet column on reads, reducing I/O for tables with large inline blobs whose payload bytes aren't needed. read_blob() continues to work under DESCRIPTOR on Parquet (auto-downgraded per scan).
  • Public API: No public API changes. HoodieReaderConfig.BLOB_INLINE_READ_MODE keeps its key, default, and valid values; only the docstring is updated.
  • Performance: For DESCRIPTOR queries on Parquet without read_blob(), the blob bytes column is no longer read or decoded. For DESCRIPTOR queries that do use read_blob(), behavior is the same as CONTENT mode (no regression).
  • Compatibility: Default remains CONTENT; existing CONTENT-mode workloads are unchanged.

Risk Level

low

BLOB_INLINE_READ_MODE defaults to CONTENT, so existing reads are unaffected. The DESCRIPTOR rewrite is gated on a metadata marker plus the user's explicit opt-in. The read_blob() override mutates a per-FileFormat flag in place, which is single-JVM-safe; concurrent queries against the same temp view are sequenced through the optimizer rule. Coverage includes COW and MOR base-file paths plus WHERE-clause and multi-column scenarios.

Documentation Update

  • HoodieReaderConfig.BLOB_INLINE_READ_MODE docstring rewritten to describe Parquet semantics and the automatic read_blob() downgrade. No website doc changes are required since this is a refinement of an existing config; if the Hudi reader-config reference page is regenerated from the source javadoc, it picks up the new text automatically.

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable

When hoodie.read.blob.inline.mode=DESCRIPTOR is set with Parquet base
files, leverage Parquet's nested column projection to skip reading the
blob 'data' sub-column entirely (genuine I/O savings). Previously the
config only affected Lance reads; Parquet still materialized the bytes.

Approach mirrors the existing VECTOR column rewrite pattern in
HoodieFileGroupReaderBasedFileFormat:
1. Detect blob columns via schema metadata
2. Strip the 'data' sub-field from blob structs in the read schema
3. Post-read null-pad the 'data' field back into output rows

Both COW (HoodieFileGroupReaderBasedFileFormat.readBaseFile) and MOR
(SparkFileFormatInternalRowReaderContext.getFileRecordIterator) paths
are covered. Also adds defensive null check in BatchedBlobReader.

read_blob() on Parquet DESCRIPTOR rows returns null since Parquet has
no byte-range blob access like Lance — documented as known limitation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added size:M PR with lines of changes in (100, 300] size:L PR with lines of changes in (300, 1000] and removed size:M PR with lines of changes in (100, 300] labels May 3, 2026
@voonhous voonhous marked this pull request as ready for review May 8, 2026 17:24

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! The PR adds Parquet DESCRIPTOR mode for blob inline reading by stripping the data sub-field from blob structs and null-padding it back post-read, with a ReadBlobRule mechanism to downgrade to CONTENT when read_blob() is in scope. The mechanism for sharing the flip across queries against the same temp view raises a couple of correctness questions worth a closer look — please see the inline comments. Once those are addressed, this should be ready for a Hudi committer or PMC member to take it from here. A couple of naming nits in ReadBlobRule — containsReadBlobAnywhere looks too similar to the existing containsReadBlobInAnyExpression (different scopes, look-alike names), and syncDescriptorMode reads like a thread-sync operation rather than a policy-apply step.

// Mutable so ReadBlobRule can flip DESCRIPTOR→CONTENT when read_blob() appears in a query.
// We mutate in place because Spark's planner/AQE retains a reference to this FileFormat
// instance even after the optimizer rewrites the LogicalRelation, so swapping wouldn't stick.
@volatile private var _isBlobDescriptorMode: Boolean = initialBlobDescriptorMode

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 I'm worried this mutable flag races across concurrent queries that share a FileFormat instance via a temp view. If query A (uses read_blob()) sets the flag to false during its optimization, and query B (no read_blob()) runs concurrently and calls restoreBlobDescriptorMode() before A's tasks are serialized, A's executors will deserialize the FileFormat with flag=true — the executor-side readBaseFile (line 565) reads isBlobDescriptorMode and would strip the data column, causing read_blob() to silently return null. Same hazard in the opposite direction for B. Async query submission (e.g. Futures, Airflow, multi-cell notebooks) makes this realistic. Have you considered passing the per-query mode through the LogicalRelation's options map / Hadoop conf at rule time instead of mutating shared state? @nsivabalan @yihua thoughts on whether this concurrency surface is acceptable here?

- AI-generated; verify before applying. React 👍/👎 to flag quality.

SparkFileFormatInternalRowReaderContext.wrapWithVectorConversion(blobPaddedIterator, vectorColumnInfo, if (blobColumnIndices.nonEmpty) parquetReadStructType else readSchema)
} else {
blobPaddedIterator
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 For MOR tables with log files, the log-file branch (line 123-126) reads with the full requiredSchema, so log records keep their populated data field, while base-file records under DESCRIPTOR get data=null. After merge the user sees a mix: records updated via log have bytes, records still in base have null. Is this the intended semantics, or should DESCRIPTOR also null the data on log-file rows for consistency?

- AI-generated; verify before applying. React 👍/👎 to flag quality.

// uses read_blob(), and restore DESCRIPTOR for queries that don't (the FileFormat instance
// is shared across queries against the same temp view).
val needsContentMode = containsReadBlobAnywhere(plan) || plan.exists(_.isInstanceOf[BatchedBlobRead])
syncDescriptorMode(plan, forceContentMode = needsContentMode)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 syncDescriptorMode is a side effect inside an optimizer rule's apply. Spark's optimizer batches typically run rules to a fixed point, so this can fire multiple times per query. It's idempotent within a single query (good), but the side effect on a shared FileFormat ordering between concurrent queries' optimizer passes is what creates the race I flagged in HoodieFileGroupReaderBasedFileFormat. Have you considered mapChildren / structural rewrites to encode the mode in the plan (e.g. as a hint or option) rather than mutating the FileFormat?

- AI-generated; verify before applying. React 👍/👎 to flag quality.

plan.expressions.exists(containsReadBlobInExpression)
}

private def containsReadBlobAnywhere(plan: LogicalPlan): Boolean = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: containsReadBlobAnywhere is dangerously close to the existing containsReadBlobInAnyExpression just above it, but does something meaningfully different — it walks the entire plan tree rather than checking only the current node's expressions. A name like planTreeContainsReadBlob or treeContainsReadBlob would make the scope distinction immediately obvious to the next person who touches this rule.

- AI-generated; verify before applying. React 👍/👎 to flag quality.


// Lance scans are skipped because their hoodieFileFormat != PARQUET and Lance already
// supports read_blob() under DESCRIPTOR via byte-range references.
private def syncDescriptorMode(plan: LogicalPlan, forceContentMode: Boolean): Unit = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: syncDescriptorMode reads like a concurrency-sync operation rather than a policy-apply step. Something like applyDescriptorModePolicy or updateDescriptorModeForPlan would make it clearer that this is deciding whether to downgrade DESCRIPTOR→CONTENT based on query content, not synchronizing threads.

- AI-generated; verify before applying. React 👍/👎 to flag quality.

@rahil-c

rahil-c commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author

We need to have some docs, and some visuals since this will be confusing for users.

I can make a seperate PR for RFC 100 cc @yihua @voonhous

@rahil-c rahil-c added this to the release-1.2.0 milestone May 13, 2026
@rahil-c rahil-c force-pushed the feat/parquet-blob-descriptor-mode branch from da8bdba to fd7c3da Compare May 14, 2026 20:39
@hudi-bot

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! The change extends DESCRIPTOR mode to Parquet base files with a clean strip-and-null-pad pattern that mirrors the existing vector rewrite, plus per-relation option routing in ReadBlobRule to preserve read_blob() semantics. One inline comment about a test annotation that may not be doing what's intended. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here. A couple of small readability suggestions — a boolean naming inconsistency and a duplicated method body across two files.

* BLOB_INLINE_READ_FORCE_CONTENT_COLUMNS option must land on the
* left table's LogicalRelation only, and the right table's payload
* must come back with DESCRIPTOR's null `data`. A bug where the
* rule writes the option to every Hudi LogicalRelation, or to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Heads-up: @Ignore is from JUnit 4 (org.junit.Ignore), but the surrounding test class uses JUnit 5 (@Test from org.junit.jupiter.api). The Jupiter engine doesn't honor JUnit 4's @Ignore, so this test will actually execute rather than be skipped — the rest of the file uses @Disabled from org.junit.jupiter.api.Disabled for that purpose. Given the TODO to re-enable comment, was the intent to skip this? If so, switching to @Disabled("reason") would do it.

- AI-generated; verify before applying. React 👍/👎 to flag quality.

@@ -237,11 +237,16 @@ abstract class HoodieBaseHadoopFsRelationFactory(val sqlContext: SQLContext,

override def buildFileFormat(): FileFormat = {
val tableConfig = metaClient.getTableConfig

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: could you rename blobDescriptorMode to isBlobDescriptorMode? It's a boolean and the constructor parameter it's passed into (isBlobDescriptorMode) already uses the is prefix — the local val not following suit is a small but jarring inconsistency.

- AI-generated; verify before applying. React 👍/👎 to flag quality.

* Wraps an iterator to re-insert null {@code data} fields into blob structs
* after Parquet DESCRIPTOR mode read (expanding 2-field → 3-field structs).
*/
private def wrapWithBlobNullPadding(iter: Iterator[InternalRow],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: the body of this wrapWithBlobNullPadding (create projection → create mapper → wrap iterator) is almost identical to SparkFileFormatInternalRowReaderContext.wrapWithBlobNullPadding at line 421 of that file — only the iterator type differs. Have you considered consolidating the shared setup into VectorConversionUtils or a small shared utility so the two call sites can't silently diverge if buildBlobNullPadRowMapper's signature changes?

- AI-generated; verify before applying. React 👍/👎 to flag quality.

@rahil-c

rahil-c commented May 15, 2026

Copy link
Copy Markdown
Collaborator Author

File ticket to have default inline mode to descriptor, currently its CONTENT (as for compaction we always need to materialize).

@rahil-c rahil-c removed this from the release-1.2.0 milestone May 15, 2026
+ "Lance file with the INLINE payload's position and size, so callers can defer "
+ "the byte read via read_blob().");
.withDocumentation("How Hudi interprets INLINE BLOB values on read for plain column access "
+ "(e.g. SELECT *). "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
+ "(e.g. SELECT *). "
+ "(e.g. SELECT blob_column FROM table). "

+ "the byte read via read_blob().");
.withDocumentation("How Hudi interprets INLINE BLOB values on read for plain column access "
+ "(e.g. SELECT *). "
+ "CONTENT (default) returns the raw inline bytes in the data field. "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should flip the default to DESCRIPTOR and let all table services to still use CONTENT mode.

Comment on lines +129 to +130
public static final String BLOB_INLINE_READ_FORCE_CONTENT_COLUMNS =
"hoodie.internal.read.blob.inline.force.content.columns";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that this internal config intends to differentiate the blob column reading of read_blob(col1) vs col2 in SELECT read_blob(col1), col2 FROM table so that col1 is read out with CONTENT and col2 is read out with DESCRIPTOR. However, Lance reader can only be configured with CONTENT or DESCRIPTOR mode for all blob columns. Should we revisit the expected behavior of hoodie.read.blob.inline.mode?

To simplify the experience, it makes sense for user to only have read_blob(col) or col in the same query where col is a blob column. If a mix of both exist, all blob columns are read out with content.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thus, all blob columns are read in DESCRIPTOR mode if no read_blob(col) is used. All blob columns are read in CONTENT mode if any read_blob(col) exists in the query.

}
}

@transient private var cachedBlobDetection: (StructType, Set[Int]) = _

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider thread-safe?

// Case 1: Inline — bytes are in field 1
val bytes = accessor.getBytes(blobStruct, 1)
// Case 1: Inline — bytes are in field 1 (may be null in DESCRIPTOR mode)
val bytes = if (accessor.isNullAt(blobStruct, 1)) null else accessor.getBytes(blobStruct, 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case that read_blob() silently returns null to the user instead of throwing where it should not?

} else if (blobColumns.contains(i)) {
InternalRow blobStruct = row.getStruct(i, 2);
// Expand {type, reference} → {type, null, reference}
GenericInternalRow expanded = new GenericInternalRow(3);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new GenericInternalRow(3) allocates per blob column per row on a hot path. For a 1M-row scan with 2 blob columns that's 2M short-lived heap allocations.

} else if (blobColumns.contains(i)) {
InternalRow blobStruct = row.getStruct(i, 2);
// Expand {type, reference} → {type, null, reference}
GenericInternalRow expanded = new GenericInternalRow(3);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the hardcoded 3 is fragile.

case ff: HoodieFileGroupReaderBasedFileFormat
if ff.hoodieFileFormat == HoodieFileFormat.PARQUET && ff.isBlobDescriptorMode =>
val matched: Set[String] = lr.output.collect {
case a: AttributeReference if readBlobAttrIds.contains(a.exprId) => a.name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matching on a.name is fragile if users alias blob columns (SELECT read_blob(payload) AS p ...) or if attribute names are qualified differently between the plan and the file schema. The reader side compares against parquetReadStructType.fields(idx).name (literal Parquet column name) in SparkFileFormatInternalRowReaderContext.scala:123 and HoodieFileGroupReaderBasedFileFormat.scala:510. Any divergence silently misses the override and read_blob() returns null.

} else {
val newOptions = rel.options +
(HoodieReaderConfig.BLOB_INLINE_READ_FORCE_CONTENT_COLUMNS -> matched.mkString(","))
val newRel = HadoopFsRelation(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HadoopFsRelation's constructor signature has varied across Spark 3.3/3.4/3.5 (e.g. userSpecifiedSchema was added in 3.4). If Hudi still supports the older Spark profile this will fail to compile cross-version. Verify against pom.xml's Spark profile matrix, or use rel.copy(options = newOptions) if Scala's case-class copy is available on this Spark version.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 1.29870% with 76 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.96%. Comparing base (4d0e9cd) to head (fd7c3da).
⚠️ Report is 45 commits behind head on master.

Files with missing lines Patch % Lines
.../apache/hudi/io/storage/VectorConversionUtils.java 0.00% 42 Missing ⚠️
...hudi/SparkFileFormatInternalRowReaderContext.scala 0.00% 34 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (4d0e9cd) and HEAD (fd7c3da). Click for more details.

HEAD has 32 uploads less than BASE
Flag BASE (4d0e9cd) HEAD (fd7c3da)
spark-scala-tests 12 0
spark-java-tests 18 0
common-and-other-modules 1 0
utilities 1 0
Additional details and impacted files
@@              Coverage Diff              @@
##             master   #18683       +/-   ##
=============================================
- Coverage     68.08%   53.96%   -14.12%     
+ Complexity    28940    12449    -16491     
=============================================
  Files          2519     1434     -1085     
  Lines        140646    72208    -68438     
  Branches      17427     8259     -9168     
=============================================
- Hits          95757    38968    -56789     
+ Misses        37030    29738     -7292     
+ Partials       7859     3502     -4357     
Flag Coverage Δ
common-and-other-modules ?
hadoop-mr-java-client 44.99% <100.00%> (+0.02%) ⬆️
spark-client-hadoop-common 48.29% <1.29%> (-0.15%) ⬇️
spark-java-tests ?
spark-scala-tests ?
utilities ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
.../apache/hudi/common/config/HoodieReaderConfig.java 98.14% <100.00%> (-1.86%) ⬇️
...hudi/SparkFileFormatInternalRowReaderContext.scala 3.37% <0.00%> (-73.35%) ⬇️
.../apache/hudi/io/storage/VectorConversionUtils.java 0.00% <0.00%> (-80.40%) ⬇️

... and 1868 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rahil-c rahil-c added this to the release-1.3.0 milestone May 19, 2026
@rahil-c rahil-c changed the title feat: Add Parquet DESCRIPTOR mode for blob inline reading type:story Add Parquet DESCRIPTOR mode for blob inline reading Jun 15, 2026
@rahil-c rahil-c added the type:story User story label Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L PR with lines of changes in (300, 1000] type:story User story

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants